#ifndef UTF8INI_H
#define UTF8INI_H

#include <map>
#include <string>
#include <vector>

class Utf8Ini {
 public:
  /**
  \brief Serialize to the INI file format.
  */
  inline std::string Serialize() const {
    std::string output;
    for (const auto& section : mSections) {
      if (output.length()) appendLine(output, "");
      appendLine(output, makeSectionText(section.first));
      for (const auto& keyvalue : section.second)
        if (keyvalue.first.length())
          appendLine(output, makeKeyValueText(keyvalue.first, keyvalue.second));
    }
    return output;
  }

  /**
  \brief Deserialize from the INI file format.
  \param data The INI data.
  \param [out] errorLine The error line (only has a meaning when this function
  failed). \return true if it succeeds, false if it fails.
  */
  inline bool Deserialize(const std::string& data, int& errorLine) {
    // initialize
    errorLine = 0;
    Clear();

    // read lines
    std::vector<std::string> lines;
    std::string curLine;
    for (auto ch : data) {
      switch (ch) {
        case '\r':
          break;
        case '\n':
          lines.push_back(trim(curLine));
          curLine.clear();
          break;
        default:
          curLine += ch;
      }
    }
    if (curLine.length()) lines.push_back(trim(curLine));

    // parse lines
    std::string section = "";
    for (const auto& line : lines) {
      errorLine++;
      switch (getLineType(line)) {
        case LineType::Invalid:
          return false;

        case LineType::Comment:
        case LineType::Empty:
          continue;

        case LineType::KeyValue: {
          std::string key;
          std::string value;
          if (!section.length() || !parseKeyValueLine(line, key, value) ||
              !SetValue(section, key, value))
            return false;
        } break;

        case LineType::Section: {
          if (!parseSectionLine(line, section)) return false;
        } break;

        default:
          return false;
      }
    }
    return true;
  }

  /**
  \brief Sets a value. This will overwrite older values.
  \param section The section. Must have a length greater than one.
  \param key The key. Must have a length greater than one.
  \param value The value. Can be empty (effectively removing the value).
  \return true if the value was set successfully, false otherwise.
  */
  inline bool SetValue(const std::string& section, const std::string& key,
                       const std::string& value) {
    auto trimmedSection = trim(section);
    auto trimmedKey = trim(key);
    if (!trimmedSection.length() || !trimmedKey.length()) return false;
    auto found = mSections.find(trimmedSection);
    if (found != mSections.end())
      found->second[std::move(trimmedKey)] = value;
    else {
      KeyValueMap keyValueMap;
      keyValueMap.emplace(std::move(trimmedKey), value);
      mSections.emplace(std::move(trimmedSection), std::move(keyValueMap));
    }
    return true;
  }

  /**
  \brief Removes all key/value pairs from a section.
  \param section The section to clear.
  \return true if it succeeds, false otherwise.
  */
  inline bool ClearSection(const std::string& section) {
    auto trimmedSection = trim(section);
    if (!trimmedSection.length()) return false;
    auto found = mSections.find(trimmedSection);
    if (found == mSections.end()) return false;
    mSections.erase(found);
    return true;
  }

  /**
  \brief Removes all sections.
  */
  inline void Clear() { mSections.clear(); }

  /**
  \brief Gets a value.
  \param section The section.
  \param key The key.
  \return The value. Empty string when the value was not found or empty.
  */
  inline std::string GetValue(const std::string& section,
                              const std::string& key) const {
    auto trimmedSection = trim(section);
    auto trimmedKey = trim(key);
    if (!trimmedSection.length() || !trimmedKey.length()) return "";
    auto sectionFound = mSections.find(trimmedSection);
    if (sectionFound == mSections.end()) return "";
    const auto& keyValueMap = sectionFound->second;
    auto keyFound = keyValueMap.find(trimmedKey);
    if (keyFound == keyValueMap.end()) return "";
    return keyFound->second;
  }

  /**
  \brief Gets the section names.
  \return List of section names.
  */
  inline std::vector<std::string> Sections() const {
    std::vector<std::string> sections;
    sections.reserve(mSections.size());
    for (const auto& section : mSections) sections.push_back(section.first);
    return sections;
  }

  /**
  \brief Gets keys in a given section.
  \param section The section.
  \return List of keys in the section. Empty if the section is not found or
  empty.
  */
  inline std::vector<std::string> Keys(const std::string& section) const {
    std::vector<std::string> keys;
    auto trimmedSection = trim(section);
    if (trimmedSection.length()) {
      auto found = mSections.find(trimmedSection);
      if (found != mSections.end()) {
        keys.reserve(found->second.size());
        for (const auto& key : found->second) keys.push_back(key.first);
      }
    }
    return keys;
  }

 private:
  typedef std::map<std::string, std::string> KeyValueMap;
  std::map<std::string, KeyValueMap> mSections;

  enum class LineType { Invalid, Empty, Section, KeyValue, Comment };

  static inline LineType getLineType(const std::string& line) {
    auto len = line.length();
    if (!len) return LineType::Empty;
    if (line[0] == '[' && line[len - 1] == ']') return LineType::Section;
    if (line[0] == ';') return LineType::Comment;
    if (line.find('=') != std::string::npos) return LineType::KeyValue;
    return LineType::Invalid;
  }

  static inline std::string trim(const std::string& str) {
    auto len = str.length();
    if (!len) return "";
    size_t pre = 0;
    while (str[pre] == ' ') pre++;
    size_t post = 0;
    while (str[len - post - 1] == ' ' && post < len) post++;
    auto sublen = len - post - pre;
    return sublen > 0 ? str.substr(pre, len - post - pre) : "";
  }

  static inline bool parseKeyValueLine(const std::string& line,
                                       std::string& key, std::string& value) {
    auto pos = line.find('=');
    key = trim(line.substr(0, pos));
    value = trim(line.substr(pos + 1));
    auto len = value.length();
    if (len && value[0] == '\"' && value[len - 1] == '\"')
      value = unescapeValue(value);
    return true;
  }

  static inline bool parseSectionLine(const std::string& line,
                                      std::string& section) {
    section = trim(line.substr(1, line.length() - 2));
    return section.length() > 0;
  }

  static inline void appendLine(std::string& output, const std::string& line) {
    if (output.length()) output += "\r\n";
    output += line;
  }

  static inline std::string makeSectionText(const std::string& section) {
    return "[" + section + "]";
  }

  static inline std::string makeKeyValueText(const std::string& key,
                                             const std::string& value) {
    return key + "=" + escapeValue(value);
  }

  static inline bool needsEscaping(const std::string& value) {
    auto len = value.length();
    return len && (value[0] == ' ' || value[len - 1] == ' ' ||
                   value.find('\n') != std::string::npos ||
                   value.find('\"') != std::string::npos);
  }

  static inline std::string escapeValue(const std::string& value) {
    if (!needsEscaping(value)) return value;
    std::string escaped = "\"";
    for (auto ch : value) {
      switch (ch) {
        case '\"':
          escaped += "\\\"";
          break;
        case '\\':
          escaped += "\\\\";
          break;
        case '\r':
          escaped += "\\r";
          break;
        case '\n':
          escaped += "\\n";
          break;
        case '\t':
          escaped += "\\t";
          break;
        default:
          escaped += ch;
      }
    }
    escaped += "\"";
    return escaped;
  }

  static inline std::string unescapeValue(const std::string& str) {
    std::string result;
    auto bEscaped = false;
    for (auto ch : str) {
      if (!bEscaped) {
        switch (ch) {
          case '\"':
            break;
          case '\\':
            bEscaped = true;
            break;
          default:
            result += ch;
        }
      } else {
        switch (ch) {
          case 'r':
            result += '\r';
            break;
          case 'n':
            result += '\n';
            break;
          case 't':
            result += '\t';
            break;
          default:
            result += ch;
        }
        bEscaped = false;
      }
    }
    if (bEscaped) result += '\\';
    return result;
  }
};

#endif  // UTF8INI_H